/*******************************************************************************
        
        copyright:		Copyright (C) Thomas Dixon 2008. All rights reserved
        				
        license:		BSD style: $(LICENSE)
        		
        author(s): 		Original version by Thomas Dixon
        				Modified by Mike Reinhold and Phil DeMonaco.
        				
*******************************************************************************/
module tango.util.cipher.TEA;

private import tango.util.cipher.Cipher;

/** Implementation of the TEA cipher designed by
 David Wheeler and Roger Needham. */
class TEA: BlockCipher {
	private {
		static const uint ROUNDS = 32, KEY_SIZE = 16, BLOCK_SIZE = 8, 
		IV_SIZE = 8, DELTA = 0x9e3779b9u, DECRYPT_SUM = 0xc6ef3720u;
		uint sk0, sk1, sk2, sk3, sum;
		static const uint[] KEY_SIZES = [16];
	}

	this(){
	}

	this(TEA other){
		this.sk0 = other.sk0;
		this.sk1 = other.sk1;
		this.sk2 = other.sk2;
		this.sk3 = other.sk3;
		this.sum = other.sum;
		this._initialized = other._initialized;
		this._encrypt = other._encrypt;
	}
	
	TEA dup() {
		return new TEA(this);
	}
	
	/***************************************************************************
	
			reset
									
			Remarks:
			unused

	***************************************************************************/
	final override void reset() {
	}


	/***************************************************************************
			
			name
			
			Returns:
			the string name of the cipher algorithm implemented by this Cipher.
					
	***************************************************************************/
	final override string name() {
		return "TEA";
	}


	/***************************************************************************
	
			blockSize
			
			Returns:
			size in bytes per block encrypted by this cipher.
	
	***************************************************************************/
	final override uint blockSize() {
		return BLOCK_SIZE;
	}
	
	/***************************************************************************
		
			ivSize
			
			Returns:
			size of the initialization vector used by this cipher.
	
	***************************************************************************/
	uint ivSize(){
		return IV_SIZE;
	}
	
	/***************************************************************************
	
			validKeySizes
						
			Returns:
			a dynamic array containing the key sizes which are valid 
			for this cipher.
	
	***************************************************************************/
	uint[] validKeySizes(){
		return KEY_SIZES;
	}

	/***************************************************************************
	
			init
			
			Params:
			encrypt =	specifies the cipher mode
							true -> encrypt
							false -> decrypt
			keyParams =	SymmetricKey object containing the key material for
						use by the cipher
			
	***************************************************************************/	
	final void init(bool encrypt, SymmetricKey keyParams) {
		_encrypt = encrypt;

		if(keyParams.key.length != KEY_SIZE)
			invalid(name() ~ ": Invalid key length (requires 16 bytes)");

		sk0 = ByteConverter.BigEndian.to!(uint)(keyParams.key[0 .. 4]);
		sk1 = ByteConverter.BigEndian.to!(uint)(keyParams.key[4 .. 8]);
		sk2 = ByteConverter.BigEndian.to!(uint)(keyParams.key[8 .. 12]);
		sk3 = ByteConverter.BigEndian.to!(uint)(keyParams.key[12 .. 16]);

		_initialized = true;
	}


	/***************************************************************************
	
			update
			
			Params:
			input_ =	array containing a single data block
			output_ =	output for the resulting data
						
			Remarks:
			updates the state of the given input block based on the 
			mode specified at the call of init(). The resulting plaintext
			or ciphertext is stored in output_. If either input_ or output_
			are smaller than BLOCK_SIZE an exception will be thrown.

	***************************************************************************/
	final override uint update(void[] input_, void[] output_) {
		if(!_initialized)
			invalid(name() ~ ": Cipher not initialized");

		ubyte[] input = cast(ubyte[]) input_, output = cast(ubyte[]) output_;

		if(input.length < BLOCK_SIZE)
			invalid(name() ~ ": Input buffer too short");

		if(output.length < BLOCK_SIZE)
			invalid(name() ~ ": Output buffer too short");

		uint v0 = ByteConverter.BigEndian.to!(uint)(input[0 .. 4]),
				v1 = ByteConverter.BigEndian.to!(uint)(input[4 .. 8]);

		sum = _encrypt ? 0 : DECRYPT_SUM;
		for(int i = 0; i < ROUNDS; i++) {
			if(_encrypt) {
				sum += DELTA;
				v0 += ((v1 << 4) + sk0) ^ (v1 + sum) ^ ((v1 >> 5) + sk1);
				v1 += ((v0 << 4) + sk2) ^ (v0 + sum) ^ ((v0 >> 5) + sk3);
			} else {
				v1 -= ((v0 << 4) + sk2) ^ (v0 + sum) ^ ((v0 >> 5) + sk3);
				v0 -= ((v1 << 4) + sk0) ^ (v1 + sum) ^ ((v1 >> 5) + sk1);
				sum -= DELTA;
			}
		}

		output[0 .. 4] = ByteConverter.BigEndian.from!(uint)(v0);
		output[4 .. 8] = ByteConverter.BigEndian.from!(uint)(v1);

		return BLOCK_SIZE;
	}

	/** Some TEA test vectors. */
	debug(UnitTest) {
		unittest {
			static string[] test_keys = ["00000000000000000000000000000000",
					"00000000000000000000000000000000",
					"0123456712345678234567893456789a",
					"0123456712345678234567893456789a"];

			static string[] test_plaintexts = ["0000000000000000",
					"0102030405060708", "0000000000000000", "0102030405060708"];

			static string[] test_ciphertexts = ["41ea3a0a94baa940",
					"6a2f9cf3fccf3c55", "34e943b0900f5dcb", "773dc179878a81c0"];

			TEA t = new TEA();
			foreach(uint i, string test_key; test_keys) {
				ubyte[] buffer = new ubyte[t.blockSize];
				string result;
				SymmetricKey key = new SymmetricKey(ByteConverter.hexDecode(
						test_key));

				// Encryption
				t.init(true, key);
				t.update(ByteConverter.hexDecode(test_plaintexts[i]), buffer);
				result = ByteConverter.hexEncode(buffer);
				assert(result == test_ciphertexts[i], t.name ~ ": (" ~ result ~ ") != (" ~ test_ciphertexts[i] ~ ")");

				// Decryption
				t.init(false, key);
				t.update(ByteConverter.hexDecode(test_ciphertexts[i]), buffer);
				result = ByteConverter.hexEncode(buffer);
				assert(result == test_plaintexts[i], t.name ~ ": (" ~ result ~ ") != (" ~ test_plaintexts[i] ~ ")");
			}
		}
	}
}
